Skip to content

An idle chat on a paired computer hands its hold to the relay, and opening it elsewhere takes it - #2287

Draft
AbirAbbas wants to merge 21 commits into
app/v3from
feat/idle-chat-releases-to-relay
Draft

AbirAbbas wants to merge 21 commits into
app/v3from
feat/idle-chat-releases-to-relay

Conversation

@AbirAbbas

Copy link
Copy Markdown
Collaborator

An idle chat on a paired computer now hands its hold to the relay. Opening it on another paired computer takes it over without the Continue-here step, even when the first computer is off. A running chat still asks, so you can watch it without taking it.

Base is app/v3, not dev. The take-back path this builds on exists only on app/v3: ConfirmWork reacquire (#2186, #2180, #1991) and the desktop engine. This change reaches dev at the next app/v3 ↔ dev merge.

What changes

  • Release when idle. A chat on a computer in a paired set (two or more computers on one identity) lets go of its hold about 2 seconds after it goes idle, once everything is on the relay: turns, settings saved after the last turn, workspace and vault. Work keeps the hold exactly as today. That covers a running turn, queued or steering work, a task that is starting or parked, jobs, title and memory writers, and unsent turns. Uploads run outside the admission lock, the final release step is short and bounded, and Close cancels and joins an attempt.
  • Take back in place. New work in the window that released (a send, queue, steer, retry, task or ! shell) retakes the hold through the existing work gate. There is no restore, no setup question, and local files such as node_modules stay. If another computer took the chat, the work gets today's held-elsewhere refusal and nothing is written.
  • Open a free chat directly. Chat rows carry an optional can_open. It is set only for paired homes, and only from the directory's own lease expiry. Opening such a chat in the terminal, or through agents.open with its id, fetches the newest saved head and takes it with a non-forcing conditional acquire, checked again at acquisition. A live hold keeps the Continue-here choice. Rebuildable files are not carried, as today.
  • Copy folder name (pre-existing, now frequent). The copy bucket was copy- plus the origin's full encoded path, so a chat moving back and forth nested the name and failed with file name too long on the third hop. Existing roots are kept, a copy origin reuses its own name, and longer names are capped at 240 bytes with a stable hash. Existing short names are unchanged.

Nothing changes for anyone else

  • This is opt-in by pairing. A home with sync off or a single device makes no new requests and keeps the same renewal cadence. A test counts every request type over a full 30 s renewal period.
  • Old builds keep working. An old holder never releases, so a new computer shows Continue here as before. An old reader sees a released chat the way it sees a lapsed hold today. can_open is optional and omitempty, and the lease rules and wire are unchanged.

Validation contract

C1. A paired, idle chat with its turns uploaded is released within a few seconds; the directory shows no live holder.
C2. A running turn, queued work, a running, starting or parked task or job, or unsent turns keep the hold, renewed as today.
C3. New work in the released window, when nobody else took the chat, retakes it before any write or model call, with no prompt.
C4. If another computer took it, new work gets the held-elsewhere refusal and nothing is written locally.
C5. Nothing is lost. Turns, journal, workspace (minus rebuildable files), memory and settings are on the relay before the release, and a failed upload keeps the hold.
C6. Opening a released or lapsed chat on another paired computer pulls and takes it with no Continue-here prompt; its state matches the last save.
C7. Opening a chat another computer holds behaves as today: you can watch, and Continue here stays the person's choice.
C8. C6 works while the first computer is stopped.
C9. Rebuildable files are not part of the hand-off.
C10. Unpaired and single-device homes behave exactly as before.
C11. Old builds on either side keep working.
C12. A send racing an open ends with exactly one owner under today's fencing.

Each item maps to TestIdleRelease_C* / TestIdleOpen_C* tests in cellsync, cellstore, session, syncsetup, chatlist, handoff, tui3 and cmd/codeaf. All run five times.

Verification

  • Focused packages are green. All contract tests passed 5×, and the cellsync/handoff/syncsetup selection passed -race -count=3. Build, vet, gofmt, the manual gates, carried help and belt pages also pass.
  • make test-laws: the lock-defer law caught three new sections, now fixed. The remaining reds fail identically on the base f7422fb8e and name no file in this change (commentlaw on factory copy, session complexity rows, taxonomy in factory).
  • Two independent review rounds and two real two-home hand walks on Linux, using real binaries, a self-hosted relay and a model stub:
    • Release timing: seal to release took 2.0–2.9 s.
    • Stopped source: the chat moved while A was stopped, with the journal exact, files present and node_modules excluded.
    • Running chat: not taken.
    • Held elsewhere: A got the refusal after B took the chat.
    • Sync-off home: unchanged.
    • The first walk found three defects: a same-window send that restored, a stale viewer that didn't pull, and the copy-folder name length. All three are fixed above with regressions. The second walk on the fixed head is in progress; results will be added here.
  • Known red on the base too: TestOwnershipCheckThatFindsAnotherHolderKeepsUnsentTurns fails on clean f7422fb8e.

Not in this PR

  • Desktop: the desktop preview needs a small change to open a can_open row directly. That will be a separate codeaf-desk PR. Until then the desktop keeps showing Continue here, which still works.
  • Mac check: pending on the exact head.

🤖 Generated with Claude Code

AbirAbbas and others added 21 commits November 3, 2026 00:21
…is on the relay

When a chat on a computer that shares its identity with another goes idle,
its saved state (turns, settings, workspace, vault) is certified and uploaded
outside the admission lock, then the hold is released in a short bounded step.
Running, queued, starting or parked work keeps the hold. New work takes the
hold back in place, or is refused if another computer took the chat. A home
with one device makes no new requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t asking

A chat whose hold was released or has lapsed is marked openable in the list,
only for paired homes and only from the directory's own clock. Opening it
fetches and takes it with a non-forcing conditional acquire, checked again at
acquisition, so a holder that renews during the fetch keeps the chat. A live
hold still offers Continue here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a free chat directly

An ordinary message in the window that released the chat reacquires the hold
through the work gate: no restore, no setup question, and local files such as
node_modules stay. A Home row for a free chat pulls the newest saved head even
when an older viewer of it is open here. Remote pre-send checks honour the
caller's deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eleased chat back

Rows carry an optional can_open. agents.open with a shared chat's id takes a
free chat without the Continue-here step. Sends, queues, steers, retries and
actions in a window that released its chat take the hold back first; app
queues and actions count as work, so they keep the hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t moves

The copy bucket was "copy-" plus the origin's full encoded path, so a chat
moving back and forth nested the name and failed with 'file name too long'
on the third hop. A chat that already has a local root keeps it, an origin
that is itself a copy reuses its own name, and longer names are capped at 240
bytes with a stable hash. Existing short names are unchanged. This predates
the idle hand-off, which makes repeated moves common. (The filing code moved
to internal/syncsetup/copy_filing.go in the previous commits.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe idle release, opening a free chat directly, take-back in the same
window, and that a running chat still asks. Add the change entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lock-defer law keeps an absorbed panic from becoming a deadlock. The
open-row lookup and the live drive's idle checks now read under a helper that
unlocks from a defer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ver undoes a take-back

The terminal seals through the warm native daemon, whose capture plan is
metadata-only, so the saved-state proof refused it and the hold was renewed
forever. Certification now asks for a complete standalone plan; ordinary
sealing stays on the daemon. A lost optimistic comparison or a competing local
vault round retries at the quiet cadence. Ownership notices carry a version,
so a release notice delivered after a newer acquisition is dropped. A held
writer whose fresh ownership read fails keeps working offline, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the idle-release vault step uses the non-blocking lock. An ordinary
SyncState waits under its own context, as on the base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nested project under a received copy keeps its own bucket, and the long-name
hash is taken from the original path rather than the lossy encoded one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d first

Adds the real-terminal test: openV3Agent and runSurface with the default
native daemon, an on-disk relay and two homes, asserting release, in-place
take-back and that effort and memory set after the last turn reach the other
computer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wnership door

Effort, model choices, approval posture, places, task effort and a remembered
note retake the hold or get the held-elsewhere refusal before they are saved,
so a save is never acknowledged where it cannot reach the relay. Unpaired and
unreleased windows keep their old path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ars a stale release snapshot

A canceled or expired Close budget skipped the base cleanup and left an
ordinary writer running; Close now keeps the idle-join error and always
runs the capture, stop, cancel and join it ran before. A zero send receipt
from the release is discarded once a reacquire has cleared the viewer, so a
send after take-back is no longer refused as "available to continue here".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ld first

Rename, rewind, task rooms, deletes and merges, plan steering, autonomy,
names, anchors, standing tree and remembered notes enter the same
released-window door before they write, so none is acknowledged where it
cannot reach the relay. Every other window returns to its old path before
any network read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Beyond 20,000 paths, 50,000 entries or 64 MiB of chat files the proof
fails and the chat keeps its hold, as before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant